ποΈGitΠ―ΡΠ°ποΈ
Commit 7bee4ffd365356ad72b4d59c7af038c3bed547e4
Parents : 2d2bd36
Author : James Rich <2199651+jamesarich@users.noreply.github.com>
Signature : Signature validation error
Date : 2026-08-02T14:57:51-05:00
Committer : GitHub <noreply@github.com>
Date : 2026-08-02T19:57:51Z
chore(build,ci): consolidate Develocity config, reuse the configuration cache, and harden CI plumbing (#6545)
Changes
32 files changed, 533 insertions(+), 382 deletions(-)
Diff
diff --git a/.github/actions/gradle-setup/action.yml b/.github/actions/gradle-setup/action.yml
index cee94ffa34..96108320b0 100644
--- a/.github/actions/gradle-setup/action.yml
+++ b/.github/actions/gradle-setup/action.yml
@@ -16,6 +16,28 @@ inputs:
develocity_access_key:
description: 'Access key for the OSS Community Develocity Instance (Build Scan publishing and remote cache writes)'
required: false
+ job_summary_pr_comment:
+ description: |
+ Job summary as a PR comment: never | always | on-failure. Needs `pull-requests: write`;
+ no-ops on fork PRs (read-only token).
+ default: 'never'
+ dependency_graph:
+ description: |
+ Dependency graph mode: disabled | generate | generate-and-submit | generate-and-upload |
+ download-and-submit. Submit needs `contents: write`. Never combine with
+ cache_configuration_cache β a CC-hit build generates NO graph.
+ default: 'disabled'
+ cache_configuration_cache:
+ description: |
+ Persist .gradle/configuration-cache. Opt-in: only pays off when config inputs are
+ commit-stable (the VERSION_CODE-pinned jobs). Real-versionCode jobs would restore,
+ miss, and never re-save.
+ default: 'false'
+ cache_key_suffix:
+ description: |
+ Extra CC-key discriminator for matrix legs running different task graphs (test-shards).
+ Runner-only matrices don't need it β os/arch are already in the key.
+ default: ''
runs:
using: composite
steps:
@@ -98,6 +120,8 @@ runs:
# Skip cleanup there β a slightly larger cache beats no cache at all.
cache-cleanup: ${{ runner.os == 'Windows' && 'never' || 'on-success' }}
add-job-summary: always
+ add-job-summary-as-pr-comment: ${{ inputs.job_summary_pr_comment }}
+ dependency-graph: ${{ inputs.dependency_graph }}
gradle-home-cache-includes: |
caches
notifications
@@ -111,3 +135,18 @@ runs:
# cache latency turns out to cost more than the restore it replaced.
gradle-home-cache-excludes: |
caches/build-cache-1
+
+ # CC entries live in the project dir; setup-gradle only caches the Gradle User Home β
+ # Develocity measured 100% CC miss (~52s config/build) before this. Runs after Setup
+ # Gradle so GRADLE_ENCRYPTION_KEY is exported. No sha in the key: unchanged build files
+ # hit exactly and skip the save; restore-keys is the self-heal for stale entries.
+ - name: Cache Gradle configuration-cache
+ # Gates: opted in; key present (undecryptable otherwise β keeps keyless fork PRs off
+ # these entries); not merge_group (throwaway cache scope).
+ if: inputs.cache_configuration_cache == 'true' && inputs.gradle_encryption_key != '' && github.event_name != 'merge_group'
+ uses: actions/cache@v6
+ with:
+ path: ${{ github.workspace }}/.gradle/configuration-cache
+ key: gradle-cc-${{ runner.os }}-${{ runner.arch }}-${{ github.job }}${{ inputs.cache_key_suffix && format('-{0}', inputs.cache_key_suffix) || '' }}-${{ hashFiles('settings.gradle.kts', '**/build.gradle.kts', 'build-logic/**', 'gradle/*.gradle', 'gradle/libs.versions.toml', 'gradle/wrapper/gradle-wrapper.properties', 'gradle.properties', 'config.properties', '.github/ci-gradle.properties') }}
+ restore-keys: |
+ gradle-cc-${{ runner.os }}-${{ runner.arch }}-${{ github.job }}${{ inputs.cache_key_suffix && format('-{0}', inputs.cache_key_suffix) || '' }}-
diff --git a/.github/workflows/create-or-promote-release.yml b/.github/workflows/create-or-promote-release.yml
index 45a8129be7..2e4e337b2f 100644
--- a/.github/workflows/create-or-promote-release.yml
+++ b/.github/workflows/create-or-promote-release.yml
@@ -82,9 +82,11 @@ jobs:
NEW_TAG="v${BASE_VERSION}-internal.${INCREMENT}"
echo "Calculated new tag: $NEW_TAG"
- echo "tag_to_process=$NEW_TAG" >> $GITHUB_OUTPUT
- echo "release_name=$NEW_TAG" >> $GITHUB_OUTPUT
- echo "final_tag=$NEW_TAG" >> $GITHUB_OUTPUT
+ {
+ echo "tag_to_process=$NEW_TAG"
+ echo "release_name=$NEW_TAG"
+ echo "final_tag=$NEW_TAG"
+ } >> "$GITHUB_OUTPUT"
else
# This is a promotion, find the latest tag from the previous channel to promote
FROM_CHANNEL="internal"
@@ -121,10 +123,12 @@ jobs:
echo "New release name will be: $NEW_TAG"
echo "Final tag will be: $NEW_TAG"
- echo "from_channel=${FROM_CHANNEL}" >> $GITHUB_OUTPUT
- echo "tag_to_process=${LATEST_TAG_TO_PROMOTE}" >> $GITHUB_OUTPUT
- echo "release_name=${NEW_TAG}" >> $GITHUB_OUTPUT
- echo "final_tag=${NEW_TAG}" >> $GITHUB_OUTPUT
+ {
+ echo "from_channel=${FROM_CHANNEL}"
+ echo "tag_to_process=${LATEST_TAG_TO_PROMOTE}"
+ echo "release_name=${NEW_TAG}"
+ echo "final_tag=${NEW_TAG}"
+ } >> "$GITHUB_OUTPUT"
fi
shell: bash
diff --git a/.github/workflows/dependency-graph-submit.yml b/.github/workflows/dependency-graph-submit.yml
new file mode 100644
index 0000000000..9badfed831
--- /dev/null
+++ b/.github/workflows/dependency-graph-submit.yml
@@ -0,0 +1,41 @@
+name: Submit Dependency Graph
+
+# PR runs can only generate-and-upload (fork tokens lack contents: write). This submits what
+# they saved, from the base repo's trusted context. Never checks out PR code β the snapshot
+# artifact is the only input.
+on:
+ workflow_run:
+ workflows: ['Pull Request CI']
+ types: [completed]
+
+permissions:
+ actions: read
+ contents: write
+
+# head_branch alone would collide across forks that share a branch name (e.g. two "patch-1"s).
+concurrency:
+ group: ${{ github.workflow }}-${{ github.event.workflow_run.head_repository.full_name }}-${{ github.event.workflow_run.head_branch }}
+ cancel-in-progress: true
+
+jobs:
+ submit-dependency-graph:
+ # failed runs may have partial graphs; skip
+ if: github.repository == 'meshtastic/Meshtastic-Android' && github.event.workflow_run.conclusion == 'success'
+ runs-on: ubuntu-24.04-arm
+ timeout-minutes: 10
+ steps:
+ # skipped android-check (docs-only/bot PRs) uploads nothing β don't fail red on that
+ - name: Check the run saved a dependency graph
+ id: probe
+ env:
+ GH_TOKEN: ${{ github.token }}
+ run: |
+ count=$(gh api --paginate "repos/${{ github.repository }}/actions/runs/${{ github.event.workflow_run.id }}/artifacts?per_page=100" \
+ --jq '[.artifacts[] | select(.name | startswith("dependency-graph"))] | length' | paste -sd+ | bc)
+ echo "count=$count" >> "$GITHUB_OUTPUT"
+
+ - name: Download and submit dependency graph
+ if: steps.probe.outputs.count != '0'
+ uses: gradle/actions/dependency-submission@v6
+ with:
+ dependency-graph: download-and-submit
diff --git a/.github/workflows/main-check.yml b/.github/workflows/main-check.yml
index 956c2ef0de..6d5e013855 100644
--- a/.github/workflows/main-check.yml
+++ b/.github/workflows/main-check.yml
@@ -31,6 +31,9 @@ jobs:
validate-and-build:
if: github.repository == 'meshtastic/Meshtastic-Android'
uses: ./.github/workflows/reusable-check.yml
+ permissions:
+ contents: write # dependency-graph submission (android-check)
+ pull-requests: write
with:
run_lint: false
run_unit_tests: true
diff --git a/.github/workflows/merge-queue.yml b/.github/workflows/merge-queue.yml
index 44e671f158..e278403c62 100644
--- a/.github/workflows/merge-queue.yml
+++ b/.github/workflows/merge-queue.yml
@@ -76,6 +76,9 @@ jobs:
needs: check-changes
if: github.repository == 'meshtastic/Meshtastic-Android' && needs.check-changes.outputs.android == 'true'
uses: ./.github/workflows/reusable-check.yml
+ permissions:
+ contents: read
+ pull-requests: write
with:
run_lint: true
run_unit_tests: true
diff --git a/.github/workflows/post-release-cleanup.yml b/.github/workflows/post-release-cleanup.yml
index ebbab71bec..e699ddb657 100644
--- a/.github/workflows/post-release-cleanup.yml
+++ b/.github/workflows/post-release-cleanup.yml
@@ -127,7 +127,7 @@ jobs:
fi
for d in "${stale[@]}"; do
- rm -rf "$work/$d"
+ rm -rf "${work:?}/$d"
done
# Rebuild versions.json (and the root placeholder) from what remains,
diff --git a/.github/workflows/promote.yml b/.github/workflows/promote.yml
index 954f3ffb8d..5fd5a6e0d8 100644
--- a/.github/workflows/promote.yml
+++ b/.github/workflows/promote.yml
@@ -90,23 +90,28 @@ jobs:
env:
INPUT_TAG_NAME: ${{ inputs.tag_name }}
run: |
- VERSION_NAME=$(echo $INPUT_TAG_NAME | sed 's/-.*//' | sed 's/v//')
- echo "APP_VERSION_NAME=$VERSION_NAME" >> $GITHUB_OUTPUT
+ VERSION_NAME=$(echo "$INPUT_TAG_NAME" | sed 's/-.*//' | sed 's/v//')
+ echo "APP_VERSION_NAME=$VERSION_NAME" >> "$GITHUB_OUTPUT"
echo "Parsed Version: $VERSION_NAME"
- name: Extract VERSION_CODE_OFFSET from config.properties
id: get_version_code_offset
run: |
OFFSET=$(grep '^VERSION_CODE_OFFSET=' config.properties | cut -d'=' -f2)
- echo "VERSION_CODE_OFFSET=$OFFSET" >> $GITHUB_OUTPUT
+ echo "VERSION_CODE_OFFSET=$OFFSET" >> "$GITHUB_OUTPUT"
- name: Calculate Version Code from Git Commit Count
id: calculate_version_code
+ env:
+ VERSION_CODE_OFFSET: ${{ steps.get_version_code_offset.outputs.VERSION_CODE_OFFSET }}
run: |
COMMIT_COUNT=$(git rev-list --count HEAD)
- OFFSET=${{ steps.get_version_code_offset.outputs.VERSION_CODE_OFFSET }}
- VERSION_CODE=$((COMMIT_COUNT + OFFSET))
- echo "versionCode=$VERSION_CODE" >> $GITHUB_OUTPUT
+ if ! [[ "$VERSION_CODE_OFFSET" =~ ^[0-9]+$ ]]; then
+ echo "::error::VERSION_CODE_OFFSET from config.properties is not numeric: '$VERSION_CODE_OFFSET'"
+ exit 1
+ fi
+ VERSION_CODE=$((COMMIT_COUNT + VERSION_CODE_OFFSET))
+ echo "versionCode=$VERSION_CODE" >> "$GITHUB_OUTPUT"
shell: bash
promote-release:
diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml
index c3223a7352..bdc3ecce29 100644
--- a/.github/workflows/pull-request.yml
+++ b/.github/workflows/pull-request.yml
@@ -112,39 +112,7 @@ jobs:
print('check-changes filter is aligned with settings.gradle module roots.')
PY
- # Drift guard: the Develocity settings plugin is applied in TWO settings
- # files β the root build and the build-logic included build β because a
- # settings `plugins {}` block cannot be sourced from an `apply(from = ...)`
- # script, so the version literal is unavoidably duplicated. Two copies of a
- # version string drift silently; this makes that a build failure instead.
- - name: Verify Develocity plugin version matches across settings files
- run: |
- python3 - <<'PY'
- import re
- from pathlib import Path
-
- PATTERN = r'id\("com\.gradle\.develocity"\)\s+version\s+"([^"]+)"'
- FILES = ('settings.gradle.kts', 'build-logic/settings.gradle.kts')
-
- found = {}
- for name in FILES:
- matches = re.findall(PATTERN, Path(name).read_text())
- if len(matches) != 1:
- print(f'{name}: expected exactly one com.gradle.develocity version '
- f'declaration, found {len(matches)}')
- raise SystemExit(1)
- found[name] = matches[0]
-
- if len(set(found.values())) != 1:
- print('Develocity plugin version drift detected:')
- for name, version in found.items():
- print(f' {name}: {version}')
- print('Both settings files must apply the same version.')
- raise SystemExit(1)
-
- print(f'Develocity plugin version is consistent: {next(iter(found.values()))}')
- PY
- # Second drift guard: the shard task lists in reusable-check.yml are
+ # Drift guard: the shard task lists in reusable-check.yml are
# hand-maintained and have silently dropped modules before (discovery,
# docs, wifi-provision, car, datastore, konsist had tests that never ran
# in CI). Every module in settings.gradle.kts must appear in the shard
@@ -224,6 +192,14 @@ jobs:
contents: read
steps:
- uses: actions/checkout@v7.0.1
+ # Workflow linting (actionlint + its shellcheck integration over every run: block).
+ # Version-pinned; the runner image ships shellcheck. ~1s over the whole tree.
+ - name: Lint GitHub workflows (actionlint)
+ run: |
+ bash <(curl -fsSL https://raw.githubusercontent.com/rhysd/actionlint/v1.7.12/scripts/download-actionlint.bash) 1.7.12 /tmp
+ /tmp/actionlint -color
+ - name: Lint repo shell scripts (shellcheck)
+ run: shellcheck scripts/*.sh
- name: Validate store listing metadata lengths
run: python3 scripts/check-metadata-length.py
# Compose Multiplatform does not strip Android-style \" / \' escapes, so a
@@ -260,6 +236,9 @@ jobs:
needs: check-changes
if: needs.check-changes.outputs.android == 'true'
uses: ./.github/workflows/reusable-check.yml
+ permissions:
+ contents: read
+ pull-requests: write # Gradle job summary as a PR comment on failure
with:
run_lint: true
run_screenshot_tests: ${{ needs.check-changes.outputs.screenshots == 'true' }}
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 89e9fd5ee8..b4b91480b5 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -102,23 +102,28 @@ jobs:
env:
INPUT_TAG_NAME: ${{ inputs.tag_name }}
run: |
- VERSION_NAME=$(echo $INPUT_TAG_NAME | sed 's/-.*//' | sed 's/v//')
- echo "APP_VERSION_NAME=$VERSION_NAME" >> $GITHUB_OUTPUT
+ VERSION_NAME=$(echo "$INPUT_TAG_NAME" | sed 's/-.*//' | sed 's/v//')
+ echo "APP_VERSION_NAME=$VERSION_NAME" >> "$GITHUB_OUTPUT"
echo "Parsed Version: $VERSION_NAME"
- name: Extract VERSION_CODE_OFFSET from config.properties
id: get_version_code_offset
run: |
OFFSET=$(grep '^VERSION_CODE_OFFSET=' config.properties | cut -d'=' -f2)
- echo "VERSION_CODE_OFFSET=$OFFSET" >> $GITHUB_OUTPUT
+ echo "VERSION_CODE_OFFSET=$OFFSET" >> "$GITHUB_OUTPUT"
- name: Calculate Version Code from Git Commit Count
id: calculate_version_code
+ env:
+ VERSION_CODE_OFFSET: ${{ steps.get_version_code_offset.outputs.VERSION_CODE_OFFSET }}
run: |
COMMIT_COUNT=$(git rev-list --count HEAD)
- OFFSET=${{ steps.get_version_code_offset.outputs.VERSION_CODE_OFFSET }}
- VERSION_CODE=$((COMMIT_COUNT + OFFSET))
- echo "versionCode=$VERSION_CODE" >> $GITHUB_OUTPUT
+ if ! [[ "$VERSION_CODE_OFFSET" =~ ^[0-9]+$ ]]; then
+ echo "::error::VERSION_CODE_OFFSET from config.properties is not numeric: '$VERSION_CODE_OFFSET'"
+ exit 1
+ fi
+ VERSION_CODE=$((COMMIT_COUNT + VERSION_CODE_OFFSET))
+ echo "versionCode=$VERSION_CODE" >> "$GITHUB_OUTPUT"
shell: bash
release-google:
@@ -152,12 +157,14 @@ jobs:
GOOGLE_PLAY_JSON_KEY: ${{ secrets.GOOGLE_PLAY_JSON_KEY }}
run: |
rm -f ./androidApp/google-services.json
- echo $GSERVICES > ./androidApp/google-services.json
- echo $KEYSTORE | base64 -di > ./androidApp/$KEYSTORE_FILENAME
+ echo "$GSERVICES" > ./androidApp/google-services.json
+ echo "$KEYSTORE" | base64 -di > "./androidApp/$KEYSTORE_FILENAME"
echo "$KEYSTORE_PROPERTIES" > ./keystore.properties
- echo "datadogApplicationId=$DATADOG_APPLICATION_ID" >> ./secrets.properties
- echo "datadogClientToken=$DATADOG_CLIENT_TOKEN" >> ./secrets.properties
- echo "MAPS_API_KEY=$GOOGLE_MAPS_API_KEY" >> ./secrets.properties
+ {
+ echo "datadogApplicationId=$DATADOG_APPLICATION_ID"
+ echo "datadogClientToken=$DATADOG_CLIENT_TOKEN"
+ echo "MAPS_API_KEY=$GOOGLE_MAPS_API_KEY"
+ } >> ./secrets.properties
echo "$GOOGLE_PLAY_JSON_KEY" > ./fastlane/play-store-credentials.json
- name: Setup Fastlane
@@ -228,7 +235,7 @@ jobs:
KEYSTORE_FILENAME: ${{ secrets.KEYSTORE_FILENAME }}
KEYSTORE_PROPERTIES: ${{ secrets.KEYSTORE_PROPERTIES }}
run: |
- echo $KEYSTORE | base64 -di > ./androidApp/$KEYSTORE_FILENAME
+ echo "$KEYSTORE" | base64 -di > "./androidApp/$KEYSTORE_FILENAME"
echo "$KEYSTORE_PROPERTIES" > ./keystore.properties
- name: Setup Fastlane
diff --git a/.github/workflows/reusable-check.yml b/.github/workflows/reusable-check.yml
index ea7cc71479..9d8d55f78b 100644
--- a/.github/workflows/reusable-check.yml
+++ b/.github/workflows/reusable-check.yml
@@ -46,9 +46,11 @@ env:
DATADOG_CLIENT_TOKEN: ${{ secrets.DATADOG_CLIENT_TOKEN }}
MAPS_API_KEY: ${{ secrets.GOOGLE_MAPS_API_KEY }}
GITHUB_TOKEN: ${{ github.token }}
- # Merge-queue and main-branch runs write to the Gradle caches; every other
- # context (PRs, release tags) is a read-only consumer.
- GRADLE_CACHE_READ_ONLY: ${{ (github.ref == 'refs/heads/main' || github.event_name == 'merge_group') && 'false' || 'true' }}
+ # Only main writes the Gradle caches. merge_group scopes are throwaway branches
+ # (per gradle/actions docs) β writes there had no reader.
+ GRADLE_CACHE_READ_ONLY: ${{ github.ref == 'refs/heads/main' && 'false' || 'true' }}
+ # Job summary as a PR comment on failure; no-ops on fork PRs (read-only token).
+ GRADLE_PR_COMMENT: ${{ github.event_name == 'pull_request' && 'on-failure' || 'never' }}
jobs:
# There is deliberately no fan-in "setup" job here. Every heavy job used to
@@ -73,6 +75,7 @@ jobs:
runs-on: ubuntu-24.04
permissions:
contents: read
+ pull-requests: write
timeout-minutes: 30
if: inputs.run_lint == true
env:
@@ -92,7 +95,9 @@ jobs:
with:
gradle_encryption_key: ${{ secrets.GRADLE_ENCRYPTION_KEY }}
develocity_access_key: ${{ secrets.DEVELOCITY_ACCESS_KEY }}
+ job_summary_pr_comment: ${{ env.GRADLE_PR_COMMENT }}
cache_read_only: ${{ env.GRADLE_CACHE_READ_ONLY }}
+ cache_configuration_cache: 'true' # VERSION_CODE pinned -> entries actually reuse
install_jetbrains_jdk: 'true'
- name: Lint, Analysis & KMP Smoke Compile
@@ -103,6 +108,7 @@ jobs:
runs-on: ubuntu-24.04
permissions:
contents: read
+ pull-requests: write
timeout-minutes: 20
if: inputs.run_lint == true && inputs.run_screenshot_tests == true
env:
@@ -120,7 +126,9 @@ jobs:
with:
gradle_encryption_key: ${{ secrets.GRADLE_ENCRYPTION_KEY }}
develocity_access_key: ${{ secrets.DEVELOCITY_ACCESS_KEY }}
+ job_summary_pr_comment: ${{ env.GRADLE_PR_COMMENT }}
cache_read_only: ${{ env.GRADLE_CACHE_READ_ONLY }}
+ cache_configuration_cache: 'true' # VERSION_CODE pinned -> entries actually reuse
- name: Screenshot Test Validation
run: ./gradlew :screenshot-tests:validateDebugScreenshotTest -Pci=true
@@ -141,6 +149,7 @@ jobs:
runs-on: ubuntu-24.04
permissions:
contents: read
+ pull-requests: write
timeout-minutes: 30
if: inputs.run_lint == true && github.event_name == 'merge_group'
@@ -156,163 +165,13 @@ jobs:
with:
gradle_encryption_key: ${{ secrets.GRADLE_ENCRYPTION_KEY }}
develocity_access_key: ${{ secrets.DEVELOCITY_ACCESS_KEY }}
+ job_summary_pr_comment: ${{ env.GRADLE_PR_COMMENT }}
cache_read_only: 'true'
- name: Verify Reproducible Build (fdroid)
env:
VERSION_CODE: ${{ github.run_number }}
- run: |
- # Comprehensive RB verification for F-Droid/IzzyOnDroid.
- # Based on: https://izzyondroid.org/docs/reproducibleBuilds/DebugFailedRBs/
- # Catches regressions that have historically broken reproducibility:
- # 1. aboutlibraries.json non-determinism (network fetching)
- # 2. Datadog buildId leaking into fdroid APK
- # 3. Google/Firebase/GMS/MLKit classes in fdroid APK
- # 4. DEPENDENCY_INFO_BLOCK in signing block
- # 5. Native library stripping (NDK version mismatch)
- # 6. aboutlibraries "generated" timestamp in res/M7.json
- # 7. baseline.prof determinism (flaky builds)
- # See: https://github.com/meshtastic/Meshtastic-Android/issues/3231
-
- echo "ββ Step 1: Verify aboutlibraries.json determinism ββ"
- rm -f androidApp/src/main/resources/aboutlibraries.json
- ./gradlew :androidApp:exportLibraryDefinitions -Pci=true --no-configuration-cache
- cp androidApp/src/main/resources/aboutlibraries.json /tmp/aboutlibraries-run1.json
-
- rm -f androidApp/src/main/resources/aboutlibraries.json
- ./gradlew :androidApp:exportLibraryDefinitions -Pci=true --no-configuration-cache --rerun-tasks
- cp androidApp/src/main/resources/aboutlibraries.json /tmp/aboutlibraries-run2.json
-
- if ! diff -q /tmp/aboutlibraries-run1.json /tmp/aboutlibraries-run2.json; then
- echo "::error::aboutlibraries.json is NOT deterministic across runs!"
- diff /tmp/aboutlibraries-run1.json /tmp/aboutlibraries-run2.json | head -20
- exit 1
- fi
- echo "β
aboutlibraries.json is deterministic"
-
- echo "ββ Step 2: Build fdroid release APK ββ"
- ./gradlew :androidApp:assembleFdroidRelease -Pci=true -Pmeshtastic.disableAbiSplits=true --no-configuration-cache
-
- APK=$(find androidApp/build/outputs/apk/fdroid/release -name "*.apk" | head -1)
- if [ -z "$APK" ]; then
- echo "::error::No fdroid release APK found"
- exit 1
- fi
- echo "Checking APK: $APK"
-
- echo "ββ Step 3: Check for datadog.buildId ββ"
- if unzip -l "$APK" | grep -q "datadog.buildId"; then
- echo "::error::fdroid APK contains assets/datadog.buildId β breaks RB!"
- exit 1
- fi
- echo "β
No datadog.buildId in fdroid APK"
-
- echo "ββ Step 4: Check for proprietary libraries (dex scan) ββ"
- TMPDIR=$(mktemp -d)
- unzip -q "$APK" -d "$TMPDIR"
- OFFENDERS=""
- for pattern in "com/google/firebase" "com/google/android/gms" "com/crashlytics" "com/google/mlkit" "com/google/android/datatransport" "androidx/privacysandbox/ads"; do
- for dex in "$TMPDIR"/classes*.dex; do
- if [ -f "$dex" ] && strings "$dex" | grep -q "L${pattern}/"; then
- OFFENDERS="${OFFENDERS}\n - $pattern"
- break
- fi
- done
- done
-
- if [ -n "$OFFENDERS" ]; then
- echo -e "::error::fdroid APK contains proprietary libraries:${OFFENDERS}"
- rm -rf "$TMPDIR"
- exit 1
- fi
- echo "β
No proprietary libraries in fdroid APK"
-
- echo "ββ Step 5: Check for DEPENDENCY_INFO_BLOCK (signing block blob) ββ"
- # Parse the APK Signing Block structure to find the dependency info pair.
- # Naive byte scans produce false positives in large APKs.
- if python3 << 'PYEOF'
- import struct, sys
-
- with open("$APK", "rb") as f:
- data = f.read()
-
- magic = b"APK Sig Block 42"
- idx = data.rfind(magic)
- if idx < 0:
- sys.exit(0)
-
- block_size = struct.unpack_from("<Q", data, idx - 8)[0]
- block_start = idx + 16 - 8 - block_size
- pos = int(block_start)
- end = idx - 8
-
- while pos + 12 <= end:
- pair_size = struct.unpack_from("<Q", data, pos)[0]
- pair_id = struct.unpack_from("<I", data, pos + 8)[0]
- if pair_id == 0x504b4453:
- print(f"DEPENDENCY_INFO_BLOCK found (id=0x{pair_id:08x})")
- sys.exit(1)
- pos += 8 + int(pair_size)
-
- sys.exit(0)
- PYEOF
- then
- echo "::error::fdroid APK contains DEPENDENCY_INFO_BLOCK β remove with dependenciesInfo { includeInApk = false }"
- rm -rf "$TMPDIR"
- exit 1
- fi
- echo "β
No DEPENDENCY_INFO_BLOCK in signing block"
-
- echo "ββ Step 6: Check native libraries have debug symbols (not stripped) ββ"
- STRIPPED_LIBS=""
- for so in $(find "$TMPDIR" -name "*.so" 2>/dev/null); do
- # If .symtab section is missing, the library was stripped
- if ! readelf -S "$so" 2>/dev/null | grep -q "\.symtab"; then
- # Libraries without symtab are stripped β this is only a problem
- # if keepDebugSymbols is not working as expected
- LIB_NAME=$(basename "$so")
- STRIPPED_LIBS="${STRIPPED_LIBS} ${LIB_NAME}"
- fi
- done
- # Note: Some third-party .so files arrive pre-stripped, which is OK.
- # We only warn here; a hard failure would be too aggressive.
- if [ -n "$STRIPPED_LIBS" ]; then
- echo "::warning::Some native libraries appear stripped (may cause NDK-version-dependent RB failures):${STRIPPED_LIBS}"
- else
- echo "β
Native libraries retain debug symbols"
- fi
-
- echo "ββ Step 7: Check aboutlibraries 'generated' timestamp not in APK ββ"
- # The M7.json (or aboutlibraries.json in Java resources) should NOT contain
- # a "generated" field, which introduces a build-time timestamp.
- ABOUT_JSON=""
- if [ -f "$TMPDIR/aboutlibraries.json" ]; then
- ABOUT_JSON="$TMPDIR/aboutlibraries.json"
- else
- # May be in res/ as M7.json or similar
- ABOUT_JSON=$(find "$TMPDIR/res" -name "*.json" -exec grep -l "aboutLibraries" {} \; 2>/dev/null | head -1)
- fi
- if [ -n "$ABOUT_JSON" ] && grep -q '"generated"' "$ABOUT_JSON"; then
- echo "::error::aboutlibraries contains 'generated' timestamp field β add excludeFields = listOf(\"generated\") to build config"
- rm -rf "$TMPDIR"
- exit 1
- fi
- echo "β
No 'generated' timestamp in aboutlibraries data"
-
- echo "ββ Step 8: Verify build from clean tree (version-control-info) ββ"
- if [ -f "$TMPDIR/META-INF/version-control-info.textproto" ]; then
- if grep -q "modified: true" "$TMPDIR/META-INF/version-control-info.textproto"; then
- echo "::warning::APK built from dirty tree (version-control-info shows modified:true). Release builds must use a clean tree."
- else
- echo "β
Built from clean tree"
- fi
- else
- echo "βΉοΈ No version-control-info.textproto (AGP may not embed it for debug-signed builds)"
- fi
-
- rm -rf "$TMPDIR"
- echo ""
- echo "π All RB checks passed"
+ run: ./scripts/verify-rb.sh
# ββ Sharded Unit Tests ββββββββββββββββββββββββββββββββββββββββββββββ
# Tests are split into 3 shards that run in parallel. Module assignment is
@@ -334,6 +193,7 @@ jobs:
runs-on: ubuntu-24.04
permissions:
contents: read
+ pull-requests: write
timeout-minutes: 45
if: inputs.run_unit_tests == true
env:
@@ -432,7 +292,11 @@ jobs:
with:
gradle_encryption_key: ${{ secrets.GRADLE_ENCRYPTION_KEY }}
develocity_access_key: ${{ secrets.DEVELOCITY_ACCESS_KEY }}
+ job_summary_pr_comment: ${{ env.GRADLE_PR_COMMENT }}
cache_read_only: ${{ env.GRADLE_CACHE_READ_ONLY }}
+ cache_configuration_cache: 'true' # VERSION_CODE pinned -> entries actually reuse
+ # Shards run different task graphs; kover flips the graph again.
+ cache_key_suffix: ${{ matrix.shard.name }}${{ inputs.run_coverage && '-kover' || '' }}
- name: Run Tests & Coverage (${{ matrix.shard.name }})
run: |
@@ -440,7 +304,11 @@ jobs:
if [[ "${{ inputs.run_coverage }}" == "true" ]]; then
kover_tasks="${{ matrix.shard.kover }}"
fi
- ./gradlew ${{ matrix.shard.tasks }} $kover_tasks -Pci=true --continue
+ # CCUD tags every shard with the same `CI job=test-shards`, which is the largest
+ # single CI cost bucket and therefore the one worth splitting. Shard identity is
+ # otherwise only recoverable by parsing the requested task list.
+ ./gradlew ${{ matrix.shard.tasks }} $kover_tasks -Pci=true --continue \
+ "-Dscan.value.CI shard=${{ matrix.shard.name }}"
- name: Upload test results to Codecov
if: ${{ !cancelled() }}
@@ -474,10 +342,11 @@ jobs:
retention-days: 7
# ββ Android Build ββββββββββββββββββββββββββββββββββββββββββββββββββββ
+ # Also generates the dependency graph β assembling both flavors resolves the widest set.
android-check:
runs-on: ubuntu-24.04
- permissions:
- contents: read
+ # No permissions block on purpose: inherits the caller's. main grants contents: write
+ # (graph submit); PRs grant read (upload only).
timeout-minutes: 60
if: inputs.run_android_build == true
@@ -494,7 +363,10 @@ jobs:
with:
gradle_encryption_key: ${{ secrets.GRADLE_ENCRYPTION_KEY }}
develocity_access_key: ${{ secrets.DEVELOCITY_ACCESS_KEY }}
+ job_summary_pr_comment: ${{ env.GRADLE_PR_COMMENT }}
cache_read_only: ${{ env.GRADLE_CACHE_READ_ONLY }}
+ # main submits; PRs upload (no contents: write), dependency-graph-submit.yml finishes.
+ dependency_graph: ${{ github.event_name == 'pull_request' && 'generate-and-upload' || (github.ref == 'refs/heads/main' && 'generate-and-submit' || 'disabled') }}
- name: Tag main-branch builds as -SNAPSHOT
if: github.event_name == 'push'
@@ -516,10 +388,12 @@ jobs:
- name: Report App Size
if: always()
run: |
- echo "### App Size Report" >> $GITHUB_STEP_SUMMARY
- echo "| Artifact | Size |" >> $GITHUB_STEP_SUMMARY
- echo "| --- | --- |" >> $GITHUB_STEP_SUMMARY
- find androidApp/build/outputs/apk -name "*.apk" -exec du -h {} + | awk '{print "| " $2 " | " $1 " |"}' >> $GITHUB_STEP_SUMMARY
+ {
+ echo "### App Size Report"
+ echo "| Artifact | Size |"
+ echo "| --- | --- |"
+ find androidApp/build/outputs/apk -name "*.apk" -exec du -h {} + | awk '{print "| " $2 " | " $1 " |"}'
+ } >> "$GITHUB_STEP_SUMMARY"
# ββ Desktop Build βββββββββββββββββββββββββββββββββββββββββββββββββββ
build-desktop:
@@ -528,6 +402,7 @@ jobs:
runs-on: ${{ matrix.os }}
permissions:
contents: read
+ pull-requests: write
timeout-minutes: 60
strategy:
fail-fast: false
@@ -547,6 +422,7 @@ jobs:
with:
gradle_encryption_key: ${{ secrets.GRADLE_ENCRYPTION_KEY }}
develocity_access_key: ${{ secrets.DEVELOCITY_ACCESS_KEY }}
+ job_summary_pr_comment: ${{ env.GRADLE_PR_COMMENT }}
cache_read_only: ${{ env.GRADLE_CACHE_READ_ONLY }}
install_jetbrains_jdk: 'true'
@@ -571,6 +447,7 @@ jobs:
runs-on: ${{ matrix.os }}
permissions:
contents: read
+ pull-requests: write
timeout-minutes: 60
strategy:
fail-fast: false
@@ -590,6 +467,7 @@ jobs:
with:
gradle_encryption_key: ${{ secrets.GRADLE_ENCRYPTION_KEY }}
develocity_access_key: ${{ secrets.DEVELOCITY_ACCESS_KEY }}
+ job_summary_pr_comment: ${{ env.GRADLE_PR_COMMENT }}
cache_read_only: true
install_jetbrains_jdk: 'true'
diff --git a/.github/workflows/scheduled-updates.yml b/.github/workflows/scheduled-updates.yml
index e163f858a3..35be52be50 100644
--- a/.github/workflows/scheduled-updates.yml
+++ b/.github/workflows/scheduled-updates.yml
@@ -209,7 +209,7 @@ jobs:
CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }}
- name: Fix file permissions
- run: sudo chown -R $USER:$USER .
+ run: sudo chown -R "$USER:$USER" .
# Early warning for overlength store-listing translations just pulled from
# Crowdin. Non-blocking on purpose: a hard failure here would abort the job
diff --git a/.github/workflows/update-changelog.yml b/.github/workflows/update-changelog.yml
index 3b14db79fa..acecc34385 100644
--- a/.github/workflows/update-changelog.yml
+++ b/.github/workflows/update-changelog.yml
@@ -66,9 +66,11 @@ jobs:
[ "$CLOSED_SHA" = "$PROD_SHA" ] && CLOSED="" && CLOSED_SHA=""
[ "$OPEN_SHA" = "$PROD_SHA" ] && OPEN="" && OPEN_SHA=""
- echo "internal=${INTERNAL:-}" >> "$GITHUB_OUTPUT"
- echo "closed=${CLOSED:-}" >> "$GITHUB_OUTPUT"
- echo "open=${OPEN:-}" >> "$GITHUB_OUTPUT"
+ {
+ echo "internal=${INTERNAL:-}"
+ echo "closed=${CLOSED:-}"
+ echo "open=${OPEN:-}"
+ } >> "$GITHUB_OUTPUT"
echo "Tags after dedup: prod=$PROD internal=$INTERNAL closed=$CLOSED open=$OPEN"
@@ -148,9 +150,9 @@ jobs:
for ((j=i+1; j<N; j++)); do
# If TAG_SHAS[j] is an ancestor of TAG_SHAS[i], swap (ancestor goes first)
if git merge-base --is-ancestor "${TAG_SHAS[$j]}" "${TAG_SHAS[$i]}" 2>/dev/null; then
- TMP_L="${TAG_LABELS[$i]}"; TAG_LABELS[$i]="${TAG_LABELS[$j]}"; TAG_LABELS[$j]="$TMP_L"
- TMP_N="${TAG_NAMES[$i]}"; TAG_NAMES[$i]="${TAG_NAMES[$j]}"; TAG_NAMES[$j]="$TMP_N"
- TMP_S="${TAG_SHAS[$i]}"; TAG_SHAS[$i]="${TAG_SHAS[$j]}"; TAG_SHAS[$j]="$TMP_S"
+ TMP_L="${TAG_LABELS[i]}"; TAG_LABELS[i]="${TAG_LABELS[j]}"; TAG_LABELS[j]="$TMP_L"
+ TMP_N="${TAG_NAMES[i]}"; TAG_NAMES[i]="${TAG_NAMES[j]}"; TAG_NAMES[j]="$TMP_N"
+ TMP_S="${TAG_SHAS[i]}"; TAG_SHAS[i]="${TAG_SHAS[j]}"; TAG_SHAS[j]="$TMP_S"
fi
done
done
diff --git a/.github/workflows/verify-flatpak.yml b/.github/workflows/verify-flatpak.yml
index 4154d4c5a7..c27d19f7ee 100644
--- a/.github/workflows/verify-flatpak.yml
+++ b/.github/workflows/verify-flatpak.yml
@@ -116,7 +116,7 @@ jobs:
env:
ARCH: ${{ matrix.arch }}
run: |
- flatpak build-bundle repo org.meshtastic.MeshtasticDesktop.${ARCH}.flatpak \
+ flatpak build-bundle repo "org.meshtastic.MeshtasticDesktop.${ARCH}.flatpak" \
org.meshtastic.MeshtasticDesktop \
--runtime-repo=https://flathub.org/repo/flathub.flatpakrepo
echo "### β
Offline Flatpak build succeeded ($ARCH)" >> "$GITHUB_STEP_SUMMARY"
diff --git a/.skills/testing-ci/SKILL.md b/.skills/testing-ci/SKILL.md
index cae4b6955b..40b2a79187 100644
--- a/.skills/testing-ci/SKILL.md
+++ b/.skills/testing-ci/SKILL.md
@@ -127,12 +127,12 @@ CI is defined in `.github/workflows/reusable-check.yml` and structured as parall
- **Robolectric SDK caching:** The `gradle-setup` composite action caches `~/.m2/repository/org/robolectric` to prevent flaky `SocketException` on SDK downloads. Cache key is `robolectric-{version}-sdk{level}` β update when bumping version or SDK level.
- **`mavenLocal()` gated:** Disabled by default to prevent CI cache poisoning. Pass `-PuseMavenLocal` for local JitPack testing.
- **JUnit parallel execution:** Enabled project-wide with classes running sequentially (`junit.jupiter.execution.parallel.mode.classes.default=same_thread`) to avoid `Dispatchers.setMain()` races. Cross-module parallelism comes from Gradle forks (`maxParallelForks`).
-- **`test-retry` plugin:** Applied to all module types (maxRetries=2, maxFailures=10).
+- **Test retry:** Develocity plugin's native retry (`develocity.testRetry` on each Test task), configured in `ProjectExtensions.kt` (maxRetries=2, maxFailures=10). Screenshot tests opt out (maxRetries=0). The standalone `org.gradle.test-retry` plugin was removed.
- **`fail-fast: false`:** Test sharding does not cancel other shards on failure.
- **Explicit Gradle task paths:** Prefer `app:lintFdroidDebug` over shorthand `lintDebug` in CI.
- **Pull request CI:** Main-only (`.github/workflows/pull-request.yml` targets `main`).
- **Merge queue hygiene:** `merge-queue.yml` cancels superseded runs for the same PR (GitHub does not auto-cancel destroyed merge-group runs) and skips the heavy pipeline for docs-only entries (`docs/**`, `*.md`). `rb-check` runs ONLY in the merge queue. `main-check.yml` passes `run_lint: false` β every main commit is a merge-queue-verified merge commit, so main pushes only rebuild the debug APKs for the snapshot release.
-- **Cache writes:** Trusted on `main` and merge queue runs; other refs use read-only cache.
+- **Cache writes:** Trusted on `main` only; merge-queue cache scopes are throwaway branches (writes unrecoverable), so the queue reads only, like all other refs.
- **Path filtering:** `check-changes` in `pull-request.yml` must include module dirs plus build/workflow entrypoints (`build-logic/**`, `gradle/**`, `.github/workflows/**`, `gradlew`, `settings.gradle.kts`, etc.).
- **AboutLibraries:** Runs in `offlineMode` by default (no GitHub/SPDX API calls). Release builds pass `-PaboutLibraries.release=true` via Fastlane/Gradle CLI to enable remote license fetching. Do NOT re-gate on `CI` or `GITHUB_TOKEN` alone.
diff --git a/build-logic/convention/build.gradle.kts b/build-logic/convention/build.gradle.kts
index 50a17a63e6..a56eb546c0 100644
--- a/build-logic/convention/build.gradle.kts
+++ b/build-logic/convention/build.gradle.kts
@@ -61,7 +61,7 @@ dependencies {
compileOnly(libs.ksp.gradlePlugin)
compileOnly(libs.androidx.room.gradlePlugin)
compileOnly(libs.spotless.gradlePlugin)
- compileOnly(libs.test.retry.gradlePlugin)
+ compileOnly(libs.develocity.gradlePlugin)
compileOnly(libs.aboutlibraries.gradlePlugin)
detektPlugins(libs.detekt.formatting)
diff --git a/build-logic/convention/src/main/kotlin/AndroidApplicationConventionPlugin.kt b/build-logic/convention/src/main/kotlin/AndroidApplicationConventionPlugin.kt
index d7bed850a6..0de1e540dd 100644
--- a/build-logic/convention/src/main/kotlin/AndroidApplicationConventionPlugin.kt
+++ b/build-logic/convention/src/main/kotlin/AndroidApplicationConventionPlugin.kt
@@ -27,7 +27,6 @@ class AndroidApplicationConventionPlugin : Plugin<Project> {
override fun apply(target: Project) {
with(target) {
apply(plugin = "com.android.application")
- apply(plugin = "org.gradle.test-retry")
apply(plugin = "meshtastic.android.lint")
apply(plugin = "meshtastic.detekt")
apply(plugin = "meshtastic.spotless")
diff --git a/build-logic/convention/src/main/kotlin/AndroidLibraryConventionPlugin.kt b/build-logic/convention/src/main/kotlin/AndroidLibraryConventionPlugin.kt
index 0b576a3372..2f85176440 100644
--- a/build-logic/convention/src/main/kotlin/AndroidLibraryConventionPlugin.kt
+++ b/build-logic/convention/src/main/kotlin/AndroidLibraryConventionPlugin.kt
@@ -29,7 +29,6 @@ class AndroidLibraryConventionPlugin : Plugin<Project> {
override fun apply(target: Project) {
with(target) {
apply(plugin = "com.android.library")
- apply(plugin = "org.gradle.test-retry")
apply(plugin = "meshtastic.android.lint")
apply(plugin = "meshtastic.detekt")
apply(plugin = "meshtastic.spotless")
diff --git a/build-logic/convention/src/main/kotlin/AndroidScreenshotConventionPlugin.kt b/build-logic/convention/src/main/kotlin/AndroidScreenshotConventionPlugin.kt
index dbcb462222..70e09b4330 100644
--- a/build-logic/convention/src/main/kotlin/AndroidScreenshotConventionPlugin.kt
+++ b/build-logic/convention/src/main/kotlin/AndroidScreenshotConventionPlugin.kt
@@ -15,12 +15,12 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import com.android.build.api.dsl.LibraryExtension
+import com.gradle.develocity.agent.gradle.test.DevelocityTestConfiguration
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.api.tasks.testing.Test
import org.gradle.kotlin.dsl.configure
import org.gradle.kotlin.dsl.withType
-import org.gradle.testretry.TestRetryTaskExtension
import org.meshtastic.buildlogic.library
import org.meshtastic.buildlogic.libs
@@ -28,7 +28,7 @@ import org.meshtastic.buildlogic.libs
* Convention for Compose Screenshot Testing modules (`:screenshot-tests`, `:docs-screenshots`).
*
* Owns the shared CST configuration: the `enableScreenshotTest` experimental flag, the test-retry opt-out (CST's custom
- * runner is incompatible with test-retry), and the Compose Multiplatform + screenshot-validation dependencies.
+ * runner is incompatible with test retry), and the Compose Multiplatform + screenshot-validation dependencies.
* Configuration-only: apply it ALONGSIDE `meshtastic.android.library[.compose]` and `com.android.compose.screenshot`,
* which the module declares itself. The `screenshotTests { imageDifferenceThreshold }` block stays in the module
* scripts β it is a DSL extension of the screenshot plugin, reachable there via generated accessors only.
@@ -41,11 +41,11 @@ class AndroidScreenshotConventionPlugin : Plugin<Project> {
experimentalProperties["android.experimental.enableScreenshotTest"] = true
}
- // CST screenshot tests use a custom runner incompatible with test-retry
- pluginManager.withPlugin("org.gradle.test-retry") {
- tasks.withType<Test>().configureEach {
- if (name.contains("ScreenshotTest", ignoreCase = true)) {
- extensions.configure<TestRetryTaskExtension> { maxRetries.set(0) }
+ // CST screenshot tests use a custom runner incompatible with test retry
+ tasks.withType<Test>().configureEach {
+ if (name.contains("ScreenshotTest", ignoreCase = true)) {
+ extensions.findByType(DevelocityTestConfiguration::class.java)?.testRetry {
+ maxRetries.set(0)
}
}
}
diff --git a/build-logic/convention/src/main/kotlin/KmpLibraryConventionPlugin.kt b/build-logic/convention/src/main/kotlin/KmpLibraryConventionPlugin.kt
index 13ad495d9a..ae698f3031 100644
--- a/build-logic/convention/src/main/kotlin/KmpLibraryConventionPlugin.kt
+++ b/build-logic/convention/src/main/kotlin/KmpLibraryConventionPlugin.kt
@@ -35,7 +35,6 @@ class KmpLibraryConventionPlugin : Plugin<Project> {
apply(plugin = "meshtastic.spotless")
apply(plugin = "meshtastic.dokka")
apply(plugin = "meshtastic.kover")
- apply(plugin = "org.gradle.test-retry")
apply(plugin = libs.plugin("mokkery").get().pluginId)
configureKotlinMultiplatform()
diff --git a/build-logic/convention/src/main/kotlin/org/meshtastic/buildlogic/ProjectExtensions.kt b/build-logic/convention/src/main/kotlin/org/meshtastic/buildlogic/ProjectExtensions.kt
index 6c7e717bbd..6f51a16ce5 100644
--- a/build-logic/convention/src/main/kotlin/org/meshtastic/buildlogic/ProjectExtensions.kt
+++ b/build-logic/convention/src/main/kotlin/org/meshtastic/buildlogic/ProjectExtensions.kt
@@ -16,6 +16,7 @@
*/
package org.meshtastic.buildlogic
+import com.gradle.develocity.agent.gradle.test.DevelocityTestConfiguration
import org.gradle.api.Project
import org.gradle.api.artifacts.ExternalModuleDependencyBundle
import org.gradle.api.artifacts.MinimalExternalModuleDependency
@@ -29,7 +30,6 @@ import org.gradle.kotlin.dsl.configure
import org.gradle.kotlin.dsl.getByType
import org.gradle.kotlin.dsl.withType
import org.gradle.plugin.use.PluginDependency
-import org.gradle.testretry.TestRetryTaskExtension
import java.io.FileInputStream
import java.util.Properties
@@ -122,14 +122,12 @@ internal fun Project.configureTestOptions() {
// tests are commented out). Disable to avoid breaking builds for modules with WIP tests.
tasks.withType<AbstractTestTask>().configureEach { failOnNoDiscoveredTests.set(false) }
- // Configure test retry if the plugin is applied
- pluginManager.withPlugin("org.gradle.test-retry") {
- tasks.withType<Test>().configureEach {
- extensions.configure<TestRetryTaskExtension> {
- maxRetries.set(MAX_TEST_RETRIES)
- maxFailures.set(MAX_TEST_FAILURES)
- failOnPassedAfterRetry.set(false)
- }
+ // Develocity-native test retry; findByType no-ops if the settings plugin isn't applied.
+ tasks.withType<Test>().configureEach {
+ extensions.findByType(DevelocityTestConfiguration::class.java)?.testRetry {
+ maxRetries.set(MAX_TEST_RETRIES)
+ maxFailures.set(MAX_TEST_FAILURES)
+ failOnPassedAfterRetry.set(false)
}
}
}
diff --git a/build-logic/settings-plugin/build.gradle.kts b/build-logic/settings-plugin/build.gradle.kts
new file mode 100644
index 0000000000..ffc61838e3
--- /dev/null
+++ b/build-logic/settings-plugin/build.gradle.kts
@@ -0,0 +1,44 @@
+/*
+ * Copyright (c) 2026 Meshtastic LLC
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+import org.jetbrains.kotlin.gradle.dsl.JvmTarget
+
+plugins { `kotlin-dsl` }
+
+group = "org.meshtastic.buildlogic"
+
+// Match build-logic/convention: target JDK 21.
+java {
+ sourceCompatibility = JavaVersion.VERSION_21
+ targetCompatibility = JavaVersion.VERSION_21
+}
+
+kotlin { compilerOptions { jvmTarget = JvmTarget.JVM_21 } }
+
+dependencies {
+ implementation(libs.develocity.gradlePlugin)
+ implementation(libs.ccud.gradlePlugin)
+}
+
+gradlePlugin {
+ plugins {
+ register("develocity") {
+ id = "meshtastic.develocity"
+ implementationClass = "MeshtasticDevelocitySettingsPlugin"
+ }
+ }
+}
diff --git a/gradle/build-cache.settings.gradle b/build-logic/settings-plugin/settings.gradle.kts
similarity index 51%
rename from gradle/build-cache.settings.gradle
rename to build-logic/settings-plugin/settings.gradle.kts
index a235c2c7aa..2c153a3110 100644
--- a/gradle/build-cache.settings.gradle
+++ b/build-logic/settings-plugin/settings.gradle.kts
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2025 Meshtastic LLC
+ * Copyright (c) 2026 Meshtastic LLC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -15,18 +15,28 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
-def develocityExtension = extensions.findByName("develocity")
+// Minimal on purpose: this build PROVIDES meshtastic.develocity, so it can't apply it to
+// itself. Its own rare builds go unscanned/uncached.
-buildCache {
- local {
- enabled = true
+pluginManagement {
+ repositories {
+ mavenCentral()
+ gradlePluginPortal()
+ maven { url = uri("../../offline-repository") }
}
+}
- if (develocityExtension != null) {
- remote(develocityExtension.buildCache) {
- enabled = true
- def accessKey = System.getenv("DEVELOCITY_ACCESS_KEY")?.trim()
- push = System.getenv("CI") != null && accessKey
+dependencyResolutionManagement {
+ repositories {
+ mavenCentral()
+ gradlePluginPortal()
+ maven { url = uri("../../offline-repository") }
+ }
+ versionCatalogs {
+ create("libs") {
+ from(files("../../gradle/libs.versions.toml"))
}
}
}
+
+rootProject.name = "settings-plugin"
diff --git a/build-logic/settings-plugin/src/main/kotlin/MeshtasticDevelocitySettingsPlugin.kt b/build-logic/settings-plugin/src/main/kotlin/MeshtasticDevelocitySettingsPlugin.kt
new file mode 100644
index 0000000000..6f624d2a28
--- /dev/null
+++ b/build-logic/settings-plugin/src/main/kotlin/MeshtasticDevelocitySettingsPlugin.kt
@@ -0,0 +1,68 @@
+/*
+ * Copyright (c) 2026 Meshtastic LLC
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+import com.gradle.develocity.agent.gradle.DevelocityConfiguration
+import org.gradle.api.Plugin
+import org.gradle.api.initialization.Settings
+
+/**
+ * Develocity in one place: scan publishing, obfuscation, build cache. Applied from BOTH
+ * settings files β included builds don't inherit the root's config, and without its own,
+ * build-logic silently drops to local-cache-only. Versions live in the catalog.
+ */
+class MeshtasticDevelocitySettingsPlugin : Plugin<Settings> {
+ override fun apply(settings: Settings): Unit = with(settings) {
+ pluginManager.apply("com.gradle.develocity")
+ pluginManager.apply("com.gradle.common-custom-user-data-gradle-plugin")
+
+ val isCI = System.getenv("CI") != null
+ val develocity = extensions.getByType(DevelocityConfiguration::class.java)
+
+ develocity.server.set("https://community.develocity.cloud")
+ develocity.projectId.set("meshtastic")
+ develocity.buildScan {
+ uploadInBackground.set(!isCI)
+ publishing.onlyIf { it.isAuthenticated }
+ // Fingerprints power cache-miss comparison (CI debugging); skip the payload locally.
+ capture { fileFingerprints.set(isCI) }
+ // Public instance: no machine identity. Constants on purpose β scans already
+ // record OS/CPU and CCUD adds CI metadata. Keep the `if` OUTSIDE the lambdas:
+ // capture-free lambdas are what the config cache can serialize.
+ obfuscation {
+ ipAddresses { addresses -> addresses.map { _ -> "0.0.0.0" } }
+ externalProcessName { "external-process" }
+ if (isCI) {
+ username { "ci" }
+ hostname { "ci-runner" }
+ } else {
+ username { "local-dev" }
+ hostname { "local-machine" }
+ }
+ }
+ }
+
+ buildCache {
+ // Off on CI: runners are ephemeral and every hit comes from the remote anyway.
+ local { isEnabled = !isCI }
+ remote(develocity.buildCache) {
+ isEnabled = true
+ val accessKey = System.getenv("DEVELOCITY_ACCESS_KEY")?.trim()
+ isPush = isCI && !accessKey.isNullOrEmpty()
+ }
+ }
+ }
+}
diff --git a/build-logic/settings.gradle.kts b/build-logic/settings.gradle.kts
index 74d4d16803..e5bd78e885 100644
--- a/build-logic/settings.gradle.kts
+++ b/build-logic/settings.gradle.kts
@@ -16,6 +16,7 @@
*/
pluginManagement {
+ includeBuild("settings-plugin")
repositories {
mavenCentral()
gradlePluginPortal()
@@ -31,14 +32,9 @@ pluginManagement {
}
plugins {
- // Version duplicated from the root settings.gradle.kts: a settings `plugins {}`
- // block is evaluated before the version catalog exists, so it cannot use `libs`.
- // A CI drift guard in .github/workflows/pull-request.yml keeps the two in sync.
- id("com.gradle.develocity") version "4.5.0"
+ id("meshtastic.develocity")
}
-val isCI = System.getenv("CI") != null
-
dependencyResolutionManagement {
repositories {
mavenCentral()
@@ -59,36 +55,5 @@ dependencyResolutionManagement {
}
}
-// build-logic is a plugin-included build with its own settings, so it does NOT inherit the
-// root build's Develocity configuration. Without this block the shared build-cache script
-// below finds no `develocity` extension and silently falls back to local-cache-only β
-// `./gradlew help --info` then reports "Using local directory build cache for build
-// ':build-logic'" with no matching remote line, while the root build gets both. Mirror the
-// root's server/project so the convention plugins share one remote cache with everything else.
-develocity {
- server = "https://community.develocity.cloud"
- projectId = "meshtastic"
- buildScan {
- publishing.onlyIf { it.isAuthenticated }
- // Mirrors the root settings β see the rationale there. Only reachable via a standalone
- // `-p build-logic` run (when included, the root build publishes the scan), but the
- // exposure would be identical, so don't leave the gap open.
- // The `if` stays outside the lambdas β see the root settings for why.
- obfuscation {
- ipAddresses { addresses -> addresses.map { _ -> "0.0.0.0" } }
- if (isCI) {
- username { "ci" }
- hostname { "ci-runner" }
- } else {
- username { "local-dev" }
- hostname { "local-machine" }
- }
- }
- }
-}
-
-// Build Cache configuration (Develocity remote cache + local)
-apply(from = "../gradle/build-cache.settings.gradle")
-
rootProject.name = "build-logic"
include(":convention")
diff --git a/build.gradle.kts b/build.gradle.kts
index c027d2da6a..7714dbc08f 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -35,7 +35,6 @@ plugins {
alias(libs.plugins.kover)
alias(libs.plugins.spotless) apply false
alias(libs.plugins.dokka)
- alias(libs.plugins.test.retry) apply false
alias(libs.plugins.meshtastic.root)
alias(libs.plugins.meshtastic.docs)
}
diff --git a/core/konsist/src/jvmTest/kotlin/org/meshtastic/core/konsist/BleAddressLoggingTest.kt b/core/konsist/src/jvmTest/kotlin/org/meshtastic/core/konsist/BleAddressLoggingTest.kt
index f73ea7268b..bd87de7185 100644
--- a/core/konsist/src/jvmTest/kotlin/org/meshtastic/core/konsist/BleAddressLoggingTest.kt
+++ b/core/konsist/src/jvmTest/kotlin/org/meshtastic/core/konsist/BleAddressLoggingTest.kt
@@ -53,6 +53,9 @@ class BleAddressLoggingTest {
*/
private fun scannedFiles() = Konsist.scopeFromProject()
.files
+ // scopeFromProject sweeps .claude/worktrees/ checkouts too; stale copies there
+ // resurface long-fixed lines as phantom offenders (paths match "/core/ble/").
+ .filterNot { "/.claude/" in it.path }
.filter { file -> scannedPathFragments.any { it in file.path } }
.filterNot { file -> identityUseAllowlist.any { file.path.endsWith(it) } }
@@ -98,6 +101,7 @@ class BleAddressLoggingTest {
val offenders =
Konsist.scopeFromProject()
.files
+ .filterNot { "/.claude/" in it.path } // see scannedFiles()
.filter { "/core/ble/" in it.path }
.flatMap { file ->
file.text.lines().withIndex().mapNotNull { (index, line) ->
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index fa27097baa..a69d4f6e89 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -33,7 +33,6 @@ junit5 = "6.1.2"
junit-platform = "6.1.2" # aligned with junit5 β JUnit Platform uses 1.x scheme
kotest = "6.2.3"
konsist = "0.17.3"
-testRetry = "1.6.5"
turbine = "1.2.1"
# Compose Screenshot Testing
@@ -102,6 +101,7 @@ takpacket-sdk = "0.8.1"
meshtastic-protobufs = "2.7.26.140-g6ceceae-SNAPSHOT"
# Gradle Plugins
+ccud = "2.8.0"
develocity = "4.5.0"
foojay-resolver = "1.0.0"
@@ -323,7 +323,8 @@ ksp-gradlePlugin = { module = "com.google.devtools.ksp:com.google.devtools.ksp.g
ksp-symbol-processing-aa-embeddable = { module = "com.google.devtools.ksp:symbol-processing-aa-embeddable", version.ref = "devtools-ksp" }
serialization-gradlePlugin = { module = "org.jetbrains.kotlin.plugin.serialization:org.jetbrains.kotlin.plugin.serialization.gradle.plugin", version.ref = "kotlin" }
spotless-gradlePlugin = { module = "com.diffplug.spotless:spotless-plugin-gradle", version.ref = "spotless" }
-test-retry-gradlePlugin = { module = "org.gradle:test-retry-gradle-plugin", version.ref = "testRetry" }
+ccud-gradlePlugin = { module = "com.gradle:common-custom-user-data-gradle-plugin", version.ref = "ccud" }
+develocity-gradlePlugin = { module = "com.gradle:develocity-gradle-plugin", version.ref = "develocity" }
aboutlibraries-gradlePlugin = { module = "com.mikepenz.aboutlibraries.plugin:aboutlibraries-plugin", version.ref = "aboutlibraries" }
jmdns = { module = "org.jmdns:jmdns", version.ref = "jmdns" }
@@ -381,7 +382,6 @@ detekt = { id = "dev.detekt", version.ref = "detekt" }
dokka = { id = "org.jetbrains.dokka", version.ref = "dokka" }
room = { id = "androidx.room3", version.ref = "room" }
spotless = { id = "com.diffplug.spotless", version.ref = "spotless" }
-test-retry = { id = "org.gradle.test-retry", version.ref = "testRetry" }
develocity = { id = "com.gradle.develocity", version.ref = "develocity" }
foojay-resolver = { id = "org.gradle.toolchains.foojay-resolver", version.ref = "foojay-resolver" }
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
index 8747320212..4e7e897967 100644
--- a/gradle/wrapper/gradle-wrapper.properties
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -1,8 +1,11 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-9.6.1-all.zip
-networkTimeout=10000
-retries=0
+# -bin: nothing here reads -all's docs/sources. Update sha256 with the URL on upgrades
+# (official .sha256 sits next to the distribution; renovate does both).
+distributionSha256Sum=9c0f7faeeb306cb14e4279a3e084ca6b596894089a0638e68a07c945a32c9e14
+distributionUrl=https\://services.gradle.org/distributions/gradle-9.6.1-bin.zip
+networkTimeout=30000
+retries=3
retryBackOffMs=500
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
diff --git a/scripts/verify-flatpak/desktop-offline.yaml b/scripts/verify-flatpak/desktop-offline.yaml
index 8b2f041fb2..4ea95f4842 100644
--- a/scripts/verify-flatpak/desktop-offline.yaml
+++ b/scripts/verify-flatpak/desktop-offline.yaml
@@ -73,8 +73,9 @@ modules:
- desktop-file-edit --set-key="Exec" --set-value="meshtastic-wrapper.sh %U"
/app/share/applications/org.meshtastic.MeshtasticDesktop.desktop
- install -Dm644 -t /app/share/metainfo desktopApp/packaging/linux/org.meshtastic.MeshtasticDesktop.metainfo.xml
- # Redirect the Gradle wrapper to the bundled distribution (no network).
- - sed -i 's|distributionUrl=.*|distributionUrl=gradle-all.zip|' gradle/wrapper/gradle-wrapper.properties
+ # Point the wrapper at the bundled zip. distributionSha256Sum stays β the vendored
+ # zip is the identical -bin artifact, so wrapper verification still passes.
+ - sed -i 's|distributionUrl=.*|distributionUrl=gradle-bin.zip|' gradle/wrapper/gradle-wrapper.properties
- echo "org.gradle.java.installations.auto-detect=false" >> gradle.properties
- echo "org.gradle.java.installations.auto-download=false" >> gradle.properties
- echo "org.gradle.java.installations.paths=/usr/lib/sdk/openjdk25/jvm/openjdk-25" >> gradle.properties
@@ -96,10 +97,10 @@ modules:
- type: dir
path: meshtastic-android
- type: file
- # Must match the version in gradle/wrapper/gradle-wrapper.properties.
- # Bumping the wrapper? Update both the URL and sha256 here.
- url: https://services.gradle.org/distributions/gradle-9.6.1-all.zip
- sha256: 61ba77b3ff7167e60962763eb4bae79db7120c189b9544358d0ade3c1e712a83
+ # Must be the EXACT distribution from gradle-wrapper.properties (version AND
+ # -bin flavor) β distributionSha256Sum verifies this file. Update together.
+ url: https://services.gradle.org/distributions/gradle-9.6.1-bin.zip
+ sha256: 9c0f7faeeb306cb14e4279a3e084ca6b596894089a0638e68a07c945a32c9e14
dest: "gradle/wrapper"
- dest-filename: "gradle-all.zip"
+ dest-filename: "gradle-bin.zip"
- flatpak-sources.json
diff --git a/scripts/verify-rb.sh b/scripts/verify-rb.sh
new file mode 100755
index 0000000000..541de86f06
--- /dev/null
+++ b/scripts/verify-rb.sh
@@ -0,0 +1,146 @@
+#!/usr/bin/env bash
+# Reproducible-build verification for F-Droid/IzzyOnDroid.
+# https://izzyondroid.org/docs/reproducibleBuilds/DebugFailedRBs/
+# https://github.com/meshtastic/Meshtastic-Android/issues/3231
+# Run from repo root with VERSION_CODE exported (CI: rb-check).
+# No pipefail on purpose: `unzip -l | grep -q` SIGPIPEs unzip on first match.
+set -eu
+
+# Created BEFORE any Gradle run β fixed /tmp paths could be symlink-squatted by build logic.
+WORKDIR=$(mktemp -d)
+trap 'rm -rf "$WORKDIR"' EXIT
+
+echo "ββ Step 1: Verify aboutlibraries.json determinism ββ"
+rm -f androidApp/src/main/resources/aboutlibraries.json
+./gradlew :androidApp:exportLibraryDefinitions -Pci=true --no-configuration-cache
+cp androidApp/src/main/resources/aboutlibraries.json "$WORKDIR/aboutlibraries-run1.json"
+
+rm -f androidApp/src/main/resources/aboutlibraries.json
+./gradlew :androidApp:exportLibraryDefinitions -Pci=true --no-configuration-cache --rerun-tasks
+cp androidApp/src/main/resources/aboutlibraries.json "$WORKDIR/aboutlibraries-run2.json"
+
+if ! diff -q "$WORKDIR/aboutlibraries-run1.json" "$WORKDIR/aboutlibraries-run2.json"; then
+ echo "::error::aboutlibraries.json is NOT deterministic across runs!"
+ diff "$WORKDIR/aboutlibraries-run1.json" "$WORKDIR/aboutlibraries-run2.json" | head -20
+ exit 1
+fi
+echo "β
aboutlibraries.json is deterministic"
+
+echo "ββ Step 2: Build fdroid release APK ββ"
+./gradlew :androidApp:assembleFdroidRelease -Pci=true -Pmeshtastic.disableAbiSplits=true --no-configuration-cache
+
+APK=$(find androidApp/build/outputs/apk/fdroid/release -name "*.apk" | head -1)
+if [ -z "$APK" ]; then
+ echo "::error::No fdroid release APK found"
+ exit 1
+fi
+echo "Checking APK: $APK"
+
+echo "ββ Step 3: Check for datadog.buildId ββ"
+if unzip -l "$APK" | grep -q "datadog.buildId"; then
+ echo "::error::fdroid APK contains assets/datadog.buildId β breaks RB!"
+ exit 1
+fi
+echo "β
No datadog.buildId in fdroid APK"
+
+echo "ββ Step 4: Check for proprietary libraries (dex scan) ββ"
+APK_DIR="$WORKDIR/apk"
+mkdir "$APK_DIR"
+unzip -q "$APK" -d "$APK_DIR"
+OFFENDERS=""
+for pattern in "com/google/firebase" "com/google/android/gms" "com/crashlytics" "com/google/mlkit" "com/google/android/datatransport" "androidx/privacysandbox/ads"; do
+ for dex in "$APK_DIR"/classes*.dex; do
+ if [ -f "$dex" ] && strings "$dex" | grep -q "L${pattern}/"; then
+ OFFENDERS="${OFFENDERS}\n - $pattern"
+ break
+ fi
+ done
+done
+
+if [ -n "$OFFENDERS" ]; then
+ echo -e "::error::fdroid APK contains proprietary libraries:${OFFENDERS}"
+ exit 1
+fi
+echo "β
No proprietary libraries in fdroid APK"
+
+echo "ββ Step 5: Check for DEPENDENCY_INFO_BLOCK (signing block blob) ββ"
+# Parse the signing block properly; naive byte scans false-positive.
+# Exit codes: 0 clean, 2 found, else parser crash.
+rc=0
+python3 - "$APK" <<'PYEOF' || rc=$?
+import struct, sys
+
+with open(sys.argv[1], "rb") as f:
+ data = f.read()
+
+magic = b"APK Sig Block 42"
+idx = data.rfind(magic)
+if idx < 0:
+ sys.exit(0)
+
+block_size = struct.unpack_from("<Q", data, idx - 8)[0]
+block_start = idx + 16 - 8 - block_size
+pos = int(block_start)
+end = idx - 8
+
+while pos + 12 <= end:
+ pair_size = struct.unpack_from("<Q", data, pos)[0]
+ pair_id = struct.unpack_from("<I", data, pos + 8)[0]
+ if pair_id == 0x504b4453:
+ print(f"DEPENDENCY_INFO_BLOCK found (id=0x{pair_id:08x})")
+ sys.exit(2)
+ pos += 8 + int(pair_size)
+
+sys.exit(0)
+PYEOF
+if [ "$rc" -eq 2 ]; then
+ echo "::error::fdroid APK contains DEPENDENCY_INFO_BLOCK β remove with dependenciesInfo { includeInApk = false }"
+ exit 1
+elif [ "$rc" -ne 0 ]; then
+ echo "::error::DEPENDENCY_INFO_BLOCK check failed to parse the APK signing block (exit $rc)"
+ exit 1
+fi
+echo "β
No DEPENDENCY_INFO_BLOCK in signing block"
+
+echo "ββ Step 6: Check native libraries have debug symbols (not stripped) ββ"
+STRIPPED_LIBS=""
+while IFS= read -r -d '' so; do
+ # no .symtab = stripped
+ if ! readelf -S "$so" 2>/dev/null | grep -q "\.symtab"; then
+ STRIPPED_LIBS="${STRIPPED_LIBS} $(basename "$so")"
+ fi
+done < <(find "$APK_DIR" -name "*.so" -print0 2>/dev/null)
+# some third-party .so arrive pre-stripped β warn only
+if [ -n "$STRIPPED_LIBS" ]; then
+ echo "::warning::Some native libraries appear stripped (may cause NDK-version-dependent RB failures):${STRIPPED_LIBS}"
+else
+ echo "β
Native libraries retain debug symbols"
+fi
+
+echo "ββ Step 7: Check aboutlibraries 'generated' timestamp not in APK ββ"
+# "generated" field = build-time timestamp
+ABOUT_JSON=""
+if [ -f "$APK_DIR/aboutlibraries.json" ]; then
+ ABOUT_JSON="$APK_DIR/aboutlibraries.json"
+else
+ ABOUT_JSON=$(find "$APK_DIR/res" -name "*.json" -exec grep -l "aboutLibraries" {} \; 2>/dev/null | head -1)
+fi
+if [ -n "$ABOUT_JSON" ] && grep -q '"generated"' "$ABOUT_JSON"; then
+ echo "::error::aboutlibraries contains 'generated' timestamp field β add excludeFields = listOf(\"generated\") to build config"
+ exit 1
+fi
+echo "β
No 'generated' timestamp in aboutlibraries data"
+
+echo "ββ Step 8: Verify build from clean tree (version-control-info) ββ"
+if [ -f "$APK_DIR/META-INF/version-control-info.textproto" ]; then
+ if grep -q "modified: true" "$APK_DIR/META-INF/version-control-info.textproto"; then
+ echo "::warning::APK built from dirty tree (version-control-info shows modified:true). Release builds must use a clean tree."
+ else
+ echo "β
Built from clean tree"
+ fi
+else
+ echo "βΉοΈ No version-control-info.textproto (AGP may not embed it for debug-signed builds)"
+fi
+
+echo ""
+echo "π All RB checks passed"
diff --git a/settings.gradle.kts b/settings.gradle.kts
index 3a2b6675be..00bd029b1d 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -16,6 +16,7 @@
*/
pluginManagement {
+ includeBuild("build-logic/settings-plugin")
includeBuild("build-logic")
repositories {
google {
@@ -32,8 +33,8 @@ pluginManagement {
}
plugins {
- id("com.gradle.develocity") version "4.5.0"
- id("com.gradle.common-custom-user-data-gradle-plugin") version "2.8.0"
+ // Develocity + CCUD + build cache; shared with build-logic, versions from the catalog.
+ id("meshtastic.develocity")
id("org.gradle.toolchains.foojay-resolver") version "1.0.0"
id("org.meshtastic.flatpak.sources.settings") version "0.1.5"
}
@@ -71,52 +72,6 @@ rootProject.name = "MeshtasticAndroid"
// https://docs.gradle.org/current/userguide/declaring_dependencies.html#sec:type-safe-project-accessors
enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS")
-val isCI = System.getenv("CI") != null
-
-develocity {
- server = "https://community.develocity.cloud"
- projectId = "meshtastic"
- buildScan {
- uploadInBackground = !isCI
- publishing.onlyIf { it.isAuthenticated }
- // File fingerprints are what let Develocity's build comparison explain a cache miss
- // down to the individual changed input. That is a CI-debugging tool, and the extra
- // scan payload is not worth paying for on every local build.
- capture { fileFingerprints = isCI }
- // community.develocity.cloud is a public OSS instance β the README badge links its
- // scan list β so no machine identity is published, from CI or a workstation.
- //
- // The workstation case is the live one: without this, every local build would publish
- // a contributor's OS username and machine hostname. The CI case is a hedge. Every
- // runner today is GitHub-hosted, so the raw hostname is an ephemeral Azure VM name
- // that leaks nothing and correlates nothing (VMs are never reused). But a self-hosted
- // runner's hostname WOULD be real infrastructure, and whoever adds one will not be
- // thinking about build scans.
- //
- // Deliberately constants, not something descriptive: the scan already records
- // `operatingSystem` ("Linux 7.0.0-28-generic (amd64)") and `numberOfCpuCores`, and the
- // common-custom-user-data plugin already adds CI workflow/job/step/run values, so an
- // "informative" hostname would only duplicate them.
- //
- // The `if` must stay OUTSIDE the lambdas. Referencing `isCI` inside one captures the
- // enclosing settings script object, which the configuration cache cannot serialize
- // ("cannot serialize Gradle script object references"). Keeping each lambda a bare
- // constant keeps it capture-free.
- obfuscation {
- ipAddresses { addresses -> addresses.map { _ -> "0.0.0.0" } }
- if (isCI) {
- username { "ci" }
- hostname { "ci-runner" }
- } else {
- username { "local-dev" }
- hostname { "local-machine" }
- }
- }
- }
-}
-
-// Build Cache configuration (Develocity remote cache + local)
-apply(from = "gradle/build-cache.settings.gradle")
@Suppress("UnstableApiUsage")
toolchainManagement {
Served by rngit 1.5.0 - Generated in 0.34s